﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class OneWayEnumerator<T> : AsyncParentEnumerator<T>
  {
    #region Static Fields

    private static readonly Guid JumpMoveAndFetchCallId = DataUtils.CreateCallId();
    private static readonly Guid GetOneWayActionCallId = DataUtils.CreateCallId();
    private static readonly Guid GetTwoWayActionCallId = DataUtils.CreateCallId();
    private static readonly Guid GetTwoWayActionReversedCallId = DataUtils.CreateCallId();

    #endregion Static Fields

    #region Constructor

    internal OneWayEnumerator( IAsyncEnumerator childEnumerator )
      : base( childEnumerator )
    {
    }

    #endregion

    public override JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      this.CheckMoveAndFetchParameters( parameters.ExpectedMoveCount, parameters.ExpectedFetchCount );

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.JumpMoveAndFetchCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      if( !this.HandleOneWayAction( asyncResult, parameters ) )
        if( !this.HandleTwoWayAction( asyncResult, parameters ) )
          throw new DataInternalException();

      return asyncResult;
    }

    public override JumpMoveAndFetchResult<T> EndJumpMoveAndFetch(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.JumpMoveAndFetchCallId );

      return asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>();
    }

    protected override void Dispose( bool disposing )
    {
      m_disposed = true;

      base.Dispose( disposing );
    }

    private bool HandleOneWayAction(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is unidirectional.
      if( ( moveCount != 0 )
        && ( fetchCount != 0 )
        && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      Func<AsyncCallback, object, IAsyncResult> begin =
        ( cb, st ) => this.BeginJumpMoveAndFetchOneWay( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> end =
        ( par, iar ) => this.EndJumpMoveAndFetchOneWay( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( begin, end );

      return true;
    }

    private bool HandleTwoWayAction(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional.
      if( ( moveCount == 0 )
        || ( fetchCount == 0 )
        || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      // Try to apply an optimized version of a bidirectional query.
      if( this.HandleTwoWayActionReversed( asyncResult, parameters ) )
        return true;

      Func<AsyncCallback, object, IAsyncResult> getPages =
        ( cb, st ) => this.BeginJumpMoveAndFetchTwoWays( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> mergePages =
        ( par, iar ) => this.EndJumpMoveAndFetchTwoWays( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( getPages, mergePages );

      return true;
    }

    private bool HandleTwoWayActionReversed(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional, but can be emulated as unidirectional.
      if( ( moveCount == 0 )
        || ( fetchCount == 0 )
        || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      // A query that starts out of bound can be optimized into an unidirectional query.
      SourcePosition sourcePosition = parameters.SourcePosition;
      if( !sourcePosition.IsOutOfBound )
      {
        long move = Math.Abs( moveCount );
        int fetch = Math.Abs( fetchCount );

        // There is no way to optimized the query since it needs to fetch items on both
        // sides of the starting position.
        if( move < fetch - 1 )
          return false;

        // A SortPosition may swallow a move to get on the first item.  We have to consider
        // this special case it may not be detected by the previous test.
        if( ( move < fetch ) && ( sourcePosition is SortPosition ) )
          return false;
      }

      Func<AsyncCallback, object, IAsyncResult> getPages =
        ( cb, st ) => this.BeginJumpMoveAndFetchTwoWaysReversed( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> mergePages =
        ( par, iar ) => this.EndJumpMoveAndFetchTwoWaysReversed( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( getPages, mergePages );

      return true;
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchOneWay(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is unidirectional.
      Debug.Assert( ( moveCount == 0 )
                 || ( fetchCount == 0 )
                 || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) );

      Func<AsyncCallback, object, IAsyncResult> begin =
        ( cb, st ) => base.BeginJumpMoveAndFetch( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> end =
        ( par, iar ) => new DependentCallResult( base.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar ) );

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetOneWayActionCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( begin, end );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchOneWay(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetOneWayActionCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchTwoWays(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional.
      Debug.Assert( ( moveCount != 0 )
                 && ( fetchCount != 0 )
                 && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) );

      // This anonymous method gets to the first desired item.
      Func<AsyncCallback, object, IAsyncResult> doMove =
        ( cb, st ) =>
        {
          JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                       parameters.SourcePosition,
                                                       moveCount,
                                                       0,
                                                       parameters.ApproximateMove,
                                                       parameters.Context );

          return this.BeginJumpMoveAndFetch( newParameters, cb, st );
        };

      // This anonymous method gets the desired items.
      Func<AsyncResult, IAsyncResult, DependentCallResult> doFetch =
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<T> moveResult = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );

          if( ( m_disposed ) || ( moveResult == null ) )
            return new DependentCallResult( null );

          if( !moveResult.Result )
            return new DependentCallResult(
                     new JumpMoveAndFetchResult<T>(
                       ( JumpMoveAndFetchAsyncResult )par,
                       moveResult.ActualMoveCount,
                       moveResult.FetchedItems,
                       moveResult.Result,
                       moveResult.EndPositionObject,
                       moveResult.EndPositionOffset,
                       moveResult.TotalCount ) );

          Func<AsyncCallback, object, IAsyncResult> beginFetch =
            ( cb, st ) =>
            {
              JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                           moveResult.EndPositionObject,
                                                           0,
                                                           fetchCount,
                                                           parameters.ApproximateMove,
                                                           parameters.Context );

              return this.BeginJumpMoveAndFetch( newParameters, cb, st );
            };

          Func<AsyncResult, IAsyncResult, DependentCallResult> endFetch =
            ( p, ar ) =>
            {
              JumpMoveAndFetchResult<T> fetchResult = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )ar );

              if( ( m_disposed ) || ( fetchResult == null ) )
                return new DependentCallResult( null );

              return new DependentCallResult(
                       new JumpMoveAndFetchResult<T>(
                         ( JumpMoveAndFetchAsyncResult )p,
                         moveResult.ActualMoveCount,
                         fetchResult.FetchedItems,
                         fetchResult.Result,
                         fetchResult.EndPositionObject,
                         fetchResult.EndPositionOffset,
                         fetchResult.TotalCount ) );
            };

          return new DependentCallResult( null, beginFetch, endFetch );
        };

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetTwoWayActionCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( doMove, doFetch );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchTwoWays(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetTwoWayActionCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchTwoWaysReversed(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;
      SourcePosition sourcePosition = parameters.SourcePosition;

      // Make sure the query is an optimizable bidirectional query.
      Debug.Assert( ( moveCount != 0 )
                 && ( fetchCount != 0 )
                 && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) );
      Debug.Assert( ( sourcePosition.IsOutOfBound )
                 || ( Math.Abs( moveCount ) >= Math.Abs( fetchCount ) - 1 )
                 || ( ( Math.Abs( moveCount ) >= Math.Abs( fetchCount ) ) && ( sourcePosition is SortPosition ) ) );

      bool failFromStart = false;
      SourcePosition endPosition = null;
      RelativeOffset endPositionOffset = null;

      // Since we are starting out of bound, we already know that at least one item 
      // will be missing at the end.
      if( ( Math.Abs( moveCount ) <= Math.Abs( fetchCount ) - 1 )
        && ( sourcePosition.IsOutOfBound ) )
      {
        int moveUnit = DataUtils.GetUnit( moveCount );
        fetchCount = Convert.ToInt32( Math.Max( 1, Math.Abs( moveCount ) ) ) * moveUnit;
        moveCount = moveUnit;

        failFromStart = true;
        endPosition = sourcePosition;
        endPositionOffset = new RelativeOffset(
                              0,
                              ( sourcePosition.IsBeforeFirst )
                                ? ReferencePoint.BeforeFirst
                                : ReferencePoint.AfterLast );
      }
      // Transform the query into an unidirectional query.
      else
      {
        moveCount += fetchCount + DataUtils.GetUnit( moveCount );
        fetchCount = -fetchCount;
      }

      // This anonymous method starts the unidirectional query.
      Func<AsyncCallback, object, IAsyncResult> getData =
        ( cb, st ) =>
        {
          JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                       sourcePosition,
                                                       moveCount,
                                                       fetchCount,
                                                       parameters.ApproximateMove,
                                                       parameters.Context );

          return this.BeginJumpMoveAndFetch( newParameters, cb, st );
        };

      // This anonymous method reverse the result set.
      Func<AsyncResult, IAsyncResult, DependentCallResult> reverseData =
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<T> result = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );

          if( ( m_disposed ) || ( result == null ) )
            return new DependentCallResult( null );

          bool success = !failFromStart && result.Result;
          Nullable<long> actualMoveCount = result.ActualMoveCount;
          ICollection<T> fetchedItems = result.FetchedItems;
          EstimateInt64 totalItemsCount = result.TotalCount;

          // We were able to move to the appropriate location.
          if( result.Result )
          {
            // Reverse the fetched items.
            List<T> currentItems = new List<T>( fetchedItems.Reverse() );
            fetchedItems = currentItems;

            // Find out the real actual move count.
            if( actualMoveCount.HasValue )
            {
              actualMoveCount = actualMoveCount.Value + result.GetFetchDisplacement();
            }

            if( !failFromStart )
            {
              endPosition = result.EndPositionObject;
              endPositionOffset = result.EndPositionOffset;

              // Find out the real end position.
              if( currentItems.Count > 1 )
              {
                endPosition = new ItemPosition( currentItems.Last() );

                if( endPositionOffset != null )
                {
                  long offset = ( currentItems.Count - 1 ) * -result.GetFetchUnit();

                  if( endPositionOffset.ReferencePosition == ReferencePoint.BeforeFirst )
                  {
                    endPositionOffset += offset;
                  }
                  else
                  {
                    endPositionOffset -= offset;
                  }
                }
              }
            }
          }
          // The move went beyond the end of the source.
          else
          {
            // Find out the real actual move count.
            if( actualMoveCount.HasValue )
            {
              ItemPosition startPosition = result.SourcePosition as ItemPosition;

              // We must adjust the move count if the starting item position wasn't in the source.
              if( ( actualMoveCount == 0 )
                && ( startPosition != null )
                && ( fetchedItems != null )
                && ( fetchedItems.Count > 0 )
                && ( !object.Equals( startPosition.Item, fetchedItems.First() ) ) )
              {
                actualMoveCount = result.GetFetchUnit();
              }

              actualMoveCount = actualMoveCount.Value + result.GetFetchDisplacement();
            }

            // Find out if we are before or after the end of source.
            if( result.ExpectedFetchCount > 0 )
            {
              endPosition = SourcePosition.AfterLast;
              endPositionOffset = new RelativeOffset( 0, ReferencePoint.AfterLast );
            }
            else
            {
              endPosition = SourcePosition.BeforeFirst;
              endPositionOffset = new RelativeOffset( 0, ReferencePoint.BeforeFirst );
            }

            // Since we went beyond the end of the source, the fetch part shouldn't have
            // occured.  We must not return any items.
            fetchedItems = null;
          }

          return new DependentCallResult(
                   new JumpMoveAndFetchResult<T>(
                     ( JumpMoveAndFetchAsyncResult )par,
                     actualMoveCount,
                     fetchedItems,
                     success,
                     endPosition,
                     endPositionOffset,
                     totalItemsCount ) );
        };

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetTwoWayActionReversedCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( getData, reverseData );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchTwoWaysReversed(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetTwoWayActionReversedCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }

    #region Private Fields

    private bool m_disposed; //false

    #endregion
  }
}
